package ie.lawlor.amvc.controller;

import ie.lawlor.amvc.IController;
import ie.lawlor.amvc.IModel;
import ie.lawlor.amvc.IView;
import ie.lawlor.amvc.Triad;
import ie.lawlor.amvc.event.Event;
import ie.lawlor.amvc.event.EventName;
import ie.lawlor.amvc.event.EventPayloadProxyFactory;
import ie.lawlor.amvc.event.IEventSink;
import ie.lawlor.amvc.event.VisibilityListener;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.log4j.Logger;

/**
 * Acts as "traffic cop" for any events entering the triad. Events can
 * be redirected to the triad's model or view, to the triads parent or 
 * to any or all of the triads children. 
 * @author blawlor
 */
public class Controller implements IController, Serializable {
  
  // These events are defined at such a low level because they are required
  // by patterns established at this level.
  public static final String CURSOR_WAITING = "cursorWaitingEvent";
  public static final String CURSOR_NORMAL = "cursorNormalEvent";
  public static final String UPDATEVIEW = "updateViewEvent";
  public static final String UPDATEMODEL = "updateModelEvent";
  public static final String EXCEPTION = "exceptionEvent";
  public static final String INITMODEL = "initModelEvent";
  public static final String INITVIEW = "initViewEvent";
  public static final String SHOWVIEW = "showViewEvent";
  public static final String HIDEVIEW = "hideViewEvent";
  
  protected Logger logger = null;
  protected IModel model;
  protected IView view = null;
  protected IController parent;
  protected HashMap children = new HashMap();
  protected String name;
  protected Map inputEvents = new HashMap();
  protected Map outputEvents = new HashMap();
  private List visibilityListeners = new ArrayList();
  
  // Map of lists of event consumers
  private Map registeredEvents = new HashMap();
  
  // The controller keeps track of visibility
  private boolean viewVisible = false;
  
  public void addOutputEvents(Map outputEvents){
    this.outputEvents.putAll(outputEvents);
  }
  
  public void addInputEvents(Map inputEvents){
    this.inputEvents.putAll(inputEvents);
  }
  
  public EventName getEvent(String key){
    EventName result = (EventName)inputEvents.get(key);
    if(result == null){
      result = (EventName)outputEvents.get(key);
    }
    return result;
  }
  
  public void setConnections(List connections){
    setTriadEvents(connections);
  }
  
  public void setModel(IModel model){
    this.model = model;
    addVisibilityListener(model);
  }
  
  public void setView(IView view){
    this.view = view;
  }
  
  /**
   * @return
   */
  public String getName() {
    return name;
  }
  
  public void setName(String name) {
    initLogger(name);
    this.name = name;
  }
  
  
  protected void emitToTriad(EventName componentEvent, EventName applicationEvent, Triad triad, boolean override){
    System.out.println(getName() + ": Routing " + componentEvent + " to triad " +
            triad.getName() + 
            " as " + applicationEvent);
    connect(componentEvent, applicationEvent, override);
    routeToTriad(applicationEvent, triad);
  }
  
  protected void emitToTriad(EventName componentEvent, EventName applicationEvent, Triad triad){
    emitToTriad(componentEvent, applicationEvent, triad, false);
  }
  
  protected void emitToTriads(EventName componentEvent, EventName applicationEvent, List triads, boolean override){
    connect(componentEvent, applicationEvent, override);
    for (ListIterator itr = triads.listIterator(); itr.hasNext();){
      Triad triad = (Triad)itr.next();
      routeToTriad(applicationEvent, triad);      
    }
  }
  
  protected void emitToTriads(EventName componentEvent, EventName applicationEvent, List triads){
    emitToTriads(componentEvent, applicationEvent, triads, false);
  }
  
  protected void emitToParent(EventName componentEvent, EventName applicationEvent, boolean override){
    System.out.println(getName() + ": Routing " + componentEvent + " to parent as " + applicationEvent);
    connect(componentEvent, applicationEvent, override);
    routeToParent(applicationEvent);
  }
  
  protected void emitAsBroadcast(EventName componentEvent, EventName applicationEvent){
    System.out.println(getName() + ": Routing " + componentEvent + " to parent and all children as " + applicationEvent);
    emitAsBroadcast(componentEvent, applicationEvent, false);
  }
  
  protected void emitAsBroadcast(EventName componentEvent, EventName applicationEvent, boolean override){
    connect(componentEvent, applicationEvent, override);
    routeToParent(applicationEvent);
    routeToChildren(applicationEvent);
  }
  
  protected void emitToParent(EventName componentEvent, EventName applicationEvent){
    emitToParent(componentEvent, applicationEvent, false);
  }
  
  protected void emitToChild(EventName componentEvent, EventName applicationEvent, String child, boolean override){
    System.out.println(getName() + ": Routing " + componentEvent + " to child " + child + " as " + applicationEvent);
    connect(componentEvent, applicationEvent, override);
    routeToChild(applicationEvent, child);
  }
  
  protected void emitToChild(EventName componentEvent, EventName applicationEvent, String child){
    emitToChild(componentEvent, applicationEvent, child, false);
  }
  
  protected void emitToChildren(EventName componentEvent, EventName applicationEvent, boolean override){
    System.out.println(getName() + ": Routing " + componentEvent + " to children as " + applicationEvent);
    connect(componentEvent, applicationEvent, override);
    routeToChildren(applicationEvent);		
  }
  
  protected void emitToChildren(EventName componentEvent, EventName applicationEvent){
    emitToChildren(componentEvent, applicationEvent, false);
  }
  
  /**
   * If an event of type 'from' arrives in the controller, it should 
   * be linked to a new event of type 'to'. 
   * @param from 
   * @param to 
   */
  private void connect(EventName from, EventName to, boolean override){
    List destinations = getDestinationsForRegistration(from);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ConnectionProxy(to));
  }
  
  private void connect(EventName from, EventName to){
    connect(from, to, false);
  }
  
  protected void terminate(EventName applicationEvent, EventName componentEvent){
    terminate(applicationEvent, componentEvent, false);
  }
  
  protected void terminate(EventName applicationEvent, EventName componentEvent, boolean override){
    System.out.println(getName() + ": Terminating " + applicationEvent + " as " + componentEvent);
    List destinations = getDestinationsForRegistration(applicationEvent);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ConnectionProxy(componentEvent));
  }
  
  protected void terminate(EventName applicationEvent, 
      EventName componentEvent, 
      boolean override,
      Object payload){
    System.out.println(getName() + ": Terminating " + applicationEvent + " as " + componentEvent + " with payload " + payload);
    List destinations = getDestinationsForRegistration(applicationEvent);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ConnectionProxy(componentEvent, payload));
  }  
  
  protected void terminate(EventName applicationEvent, 
          EventName componentEvent, 
          boolean override,
          EventPayloadProxyFactory payloadMapperFactory){
        System.out.println(getName() + ": Terminating " + applicationEvent + " as " + componentEvent + " with payloadMapperFactory " + payloadMapperFactory);
        List destinations = getDestinationsForRegistration(applicationEvent);
        if (override) {
          destinations.clear();
        }
        destinations.add(new ConnectionProxy(componentEvent, payloadMapperFactory));
      }  
  
  protected void terminate(EventName applicationEvent, EventName componentEvent, Object payload){
    terminate(applicationEvent, componentEvent, false, payload);
  }
  
  private void connect(String from, EventName to){
    connect(new EventName(from), to, false);
  }
  
  private void connect(String from, EventName to, Object newPayload){
    connect(new EventName(from), to, false);
  }
  
  protected void sendToModel(EventName eventName, boolean override) {
    System.out.println(getName() + ": Sending " + eventName + " to Model ");
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ModelProxy());
  }
  
  protected void sendToModel(EventName eventName) {
    sendToModel(eventName, false);
  }
  
  protected void sendToModel(String eventName) {
    sendToModel(new EventName(eventName), false);
  }
  
  protected void sendToView(EventName eventName, boolean override) {
    System.out.println(getName() + ": Sending " + eventName + " to View ");
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ViewProxy());
  }
  
  protected void sendToView(EventName eventName) {
    sendToView(eventName, false);
  }
  
  protected void sendToView(String eventName) {
    sendToView(new EventName(eventName), false);
  }
  
  protected void routeToTriad(EventName eventName, Triad triad, boolean override) {
    System.out.println(getName() + ": Sending " + eventName + " to triad " + triad.getName());
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new TriadProxy(triad));
  }
  
  protected void routeToTriad(EventName eventName, Triad triad) {
    routeToTriad(eventName, triad, false);
  }
  
  protected void routeToChild(EventName eventName, String childName, boolean override) {
    System.out.println(getName() + ": Routing " + eventName + " to child " + childName);
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ChildProxy(childName));
  }
  
  protected void routeToChild(EventName eventName, String childName) {
    routeToChild(eventName, childName, false);
  }
  
  // TODO Remove the 'String' event methods? Not used anyway.
  protected void routeToChild(String eventName, String childName) {
    routeToChild(new EventName(eventName), childName, false);
  }
  
  protected void routeToParent(EventName eventName, boolean override) {
    System.out.println(getName() + ": Routing " + eventName + " to parent ");
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ParentProxy());
  }
  
  protected void routeToParent(EventName eventName) {
    routeToParent(eventName, false);
  }
  
  protected void routeToParent(String eventName) {
    routeToParent(new EventName(eventName), false);
  }
  
  protected void routeToChildren(EventName eventName, boolean override) {
    System.out.println(getName() + ": Routing " + eventName + " to children ");
    List destinations = getDestinationsForRegistration(eventName);
    if (override) {
      destinations.clear();
    }
    destinations.add(new ChildrenProxy());
  }
  
  protected void routeToChildren(EventName eventName) {
    routeToChildren(eventName, false);
  }
  
  protected void routeToChildren(String eventName) {
    routeToChildren(new EventName(eventName), false);
  }
  
  private List getExisitingDestinations(EventName eventName) {
    return (List) registeredEvents.get(eventName);
  }
  
  /**
   * Gets Controller's EventSinks for this event, and if null then 
   * associates an empty collection and returns it.
   * @param eventName
   * @return
   */
  private List getDestinationsForRegistration(EventName eventName) {
    List destinations = getExisitingDestinations(eventName);
    if (destinations == null) {
      destinations = new ArrayList();
      registeredEvents.put(eventName, destinations);
    }
    return destinations;
  }
  
  private IController getChild(String childName) {
    return (IController) children.get(childName);
  }
  
  //TODO Beware event cycles!
  public void eventIn(Event e) {
    logger.debug("Entering");
    System.out.println("Controller for " + getName() + ": Handling event " + e);
    List destinations = getExisitingDestinations(e.getEventName());
    if (destinations != null) {
      logger.debug("Controller has " + destinations.size() + " destinations for " + e);
      for (Iterator itr = destinations.iterator(); itr.hasNext();) {
        IEventSink sink = (IEventSink) itr.next();
        logger.debug("Destination: " + sink);
      }
      for (Iterator itr = destinations.iterator(); itr.hasNext();) {
        IEventSink sink = (IEventSink) itr.next();
        sink.eventIn(e);
      }
    } else {
      if (parent == null){
        logger.warn("Event " + e + " not handled - falling out of application scope");
      } else {
        // Sending events to parents by default may result in an infinite
        // loop, as the event may have been send by the parent. We must catch 
        // this here and log an error.
//      if (){
//      logger.error("Controller " + getName() +
//      "is sending event " + e + 
//      " back to parent " + parent.getName() +
//      "which sent it. An infinite loop would result.");
//      }
//      logger.info("Sending " + e + " to parent by default");
//      parent.eventIn(e);
        // TODO Let's try not sending to parent by default. It may
        // be more efficient to send explicitly and the error caused 
        // by infinite loops are hard to trace.
        logger.error("Controller " + getName() + " cannot handle " + e); 
        System.out.println("Controller " + getName() + " cannot handle " + e);
      }
    }
  }
  
  public void addChild(String name, IController child) {
    child.setParent(this);
    children.put(name, child);
  }
  
  
  public void setParent(IController parent) {
    this.parent = parent;
  }
  
  public String toString(){
    return "Controller: " + name;
  }
  
  public boolean isVisible() {
    return viewVisible;
  } 
  
  // TODO Synchronize access to the visible variable?
  public void visibilityUpdated(boolean visible){
    viewVisible = visible;
    notifyVisibilityListeners(visible);
  }
  
  public void addVisibilityListener(VisibilityListener listener){
    if (!visibilityListeners.contains(listener)){
      visibilityListeners.add(listener);
    }
  }
  
  public void removeVisibilityListener(VisibilityListener listener){
    visibilityListeners.remove(listener);
  }
  
  private class ParentProxy implements IEventSink {
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to Parent");
      parent.eventIn(e);
    }
    
    public String toString(){
      return "Parent Proxy";
    }
  }
  
  private void notifyVisibilityListeners(boolean visible){
    for (ListIterator itr = visibilityListeners.listIterator(); itr.hasNext();){
      VisibilityListener listener = (VisibilityListener)itr.next();
      listener.visibilityUpdated(visible);
    }
  }
  
  private class ModelProxy implements IEventSink {
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to Model");
      if (model != null){
        model.eventIn(e);
      }
    }
    
    public String toString(){
      return "Model Proxy";
    }
  }
  
  private class ViewProxy implements IEventSink {
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to View");
      view.eventIn(e);
    }
    
    public String toString(){
      return "View Proxy";
    }
  }
  
  private class ChildrenProxy implements IEventSink {
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to Children");
      for (Iterator itr = children.values().iterator(); itr.hasNext();) {
        ((IEventSink)itr.next()).eventIn(e);
      }
    }
    
    public String toString(){
      return "Children Proxy";
    }
  }
  
  private class ChildProxy implements IEventSink {
    private String name = "";
    
    public ChildProxy(String name){
      this.name = name;
    }
    
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to Child named " + name);
      IEventSink child = (IEventSink)children.get(name);
      if (child == null){
        logger.warn("Child " + name + " of triad " + getName() + " does not exist");
      } else {
        child.eventIn(e);
      }
    }
    
    public String toString(){
      return "Child Proxy. Child name: " + name;
    }
  }
  
  private class TriadProxy implements IEventSink {
    private Triad destination;
    
    public TriadProxy(Triad destination){
      this.destination = destination;
    }
    
    public void eventIn(Event e) {
      logger.info("Sending " + e + " to Triad named " + destination.getName());
      destination.eventIn(e);
    }
    
    public String toString(){
      return "Triad Proxy. Triad name: " + destination.getName();
    }
  }
  
  private class ConnectionProxy implements IEventSink {
    
    EventName translationEventName;
    Object payload;
    EventPayloadProxyFactory payloadMappingFactory;
    public ConnectionProxy(EventName translationEventName){
      this.translationEventName = translationEventName;
    }
    
    public ConnectionProxy(EventName translationEventName, Object payload){
      this.translationEventName = translationEventName;
      this.payload = payload;
    }
    
    
    public ConnectionProxy(EventName translationEventName, EventPayloadProxyFactory payloadMappingFactory){
      this.translationEventName = translationEventName;
      this.payloadMappingFactory = payloadMappingFactory;
    }
    
    public void eventIn(Event e) {
      if (e.getEventName().equals(translationEventName)){
        logger.info("Bad connection causing infinite loop: Connecting " + e + " to " + translationEventName);
      } else {
        Event translatedEvent = new Event(e);
        if (payload != null){
          translatedEvent.setPayload(payload);
        } else if (payloadMappingFactory != null){
            translatedEvent.setPayload(payloadMappingFactory.createPayloadProxy(e.getPayload()));
        }
        System.out.println("TranslationEventName:" + translationEventName);
        translatedEvent.setEventName(translationEventName);
        logger.info("Connecting " + e + " to " + translatedEvent);
        Controller.this.eventIn(translatedEvent);
      }
    }
    
    public String toString(){
      return "Connection Proxy. Translated to " + translationEventName;
    }
  }
  
  
  protected void initLogger(String name){
    logger = Logger.getLogger("hmvc." + name + ".controller");
  }
  
  public void setTriadEvents(List events){
    for (ListIterator itr = events.listIterator(); itr.hasNext();){
      ControllerEventWrapper eventWrapper = (ControllerEventWrapper)itr.next();
      eventWrapper.addToController(this);
    }
  }
  
  /**
   * @return Returns the inputEvents.
   */
  public Map getInputEvents() {
    return inputEvents;
  }
  /**
   * @param inputEvents The inputEvents to set.
   */
  public void setInputEvents(Map inputEvents) {
    this.inputEvents.putAll(inputEvents);
  }
}